from __future__ import division
from __future__ import unicode_literals

import datetime
import logging

from google.appengine.ext import db


class ProjectModel(db.Model):

    """Details of a project found on the Cheeseshop.

    To minimize datastore writes, only properties which are needed to find out
    the top NNN apps for a statistic are indexed. Apps can always be looked up
    individually by name.

    All StringProperty instances are forced to be unicode at the point of
    creation of the entity. This is to prevent decode issues.

    """

    # Possible py3k status a project could be in, sorted from best to worst.
    PY3K_RELEASED = 'YES'
    PY3K_VCS = 'UNDER DEVELOPMENT'
    PY3K_UNKNOWN = 'UNKNOWN'
    PY3K_NOT_YET = 'NO'

    PY3K_GOOD = {PY3K_RELEASED, PY3K_VCS}

    PY3K_POSSIBILITIES = [PY3K_UNKNOWN, PY3K_NOT_YET, PY3K_VCS,
                          PY3K_RELEASED]

    name = db.StringProperty(required=True, indexed=False)  # Project name
    version = db.StringProperty(required=True, indexed=False)  # Newest version
    # Status of project
    py3k_status = db.StringProperty(default=PY3K_UNKNOWN,
                                    choices=PY3K_POSSIBILITIES, indexed=False)
    proof = db.LinkProperty(indexed=False)  # URL to proof of the status
    download_total = db.IntegerProperty()  # Total downloads for newest release
    download_rate = db.FloatProperty()  # Average daily downloads
    # How stale info is
    last_modified = db.DateTimeProperty(auto_now=True, indexed=False)

    @classmethod
    def get_for_mutating(cls, name, version):
        """Create/get a project's model.

        As this is the point of creation for entities, guarantee that unicode
        is used throughout the entity.

        """
        if isinstance(name, str):
            name = name.decode('utf-8')
        if isinstance(version, str):
            version = version.decode('utf-8')
        # Guarantees some entity will be returned
        entity = cls.get_or_insert(name, name=name, version=version)
        entity.version = version  # Can change between days
        # XXX Remove later
        if entity.py3k_status == self.PY3K_UNKNOWN and self.proof is not None:
            self.proof = None
        return entity

    @property
    def cheeseshop_url(self):
        """Return the URL to the project's page on the Cheeseshop."""
        return 'http://cheeseshop.python.org/pypi/{}/{}'.format(self.name,
                                                                self.version)

    def set_status(self, status, proof=None):
        """Set the status of the project.

        To prevent accidental shifting to a worse state, it is impossible to
        move backwards. The thinking is that even if a project chooses to pull
        support there is at least one version out there that supports Python 3.

        """
        if status not in self.PY3K_POSSIBILITIES:
            raise ValueError('{} not in {}'.format(status,
                                                   self.PY3K_POSSIBILITIES))
        currently = self.PY3K_POSSIBILITIES.index(self.py3k_status)
        proposing = self.PY3K_POSSIBILITIES.index(status)
        if  currently > proposing:
            return
        self.py3k_status = status
        if proof:
            self.proof = db.Link(proof)

    def check_classifiers(self, release_data):
        """See if the trove classifiers specify Python 3 support."""
        status = self.PY3K_UNKNOWN
        classifiers = release_data.get('classifiers')
        if classifiers is None:
            logging.warning('No classifiers for {} {}'.format(self.name,
                                                              self.version))
            return
        for classifier in classifiers:
            parts = [x.strip() for x in classifier.split('::')]
            if (len(parts) != 3 or parts[0] != 'Programming Language' or
                    parts[1] != 'Python'):
                continue
            version_supported = parts[2]
            if version_supported.startswith('3'):
                status = self.PY3K_RELEASED
                break
            elif version_supported.startswith('2'):
                status = self.PY3K_NOT_YET
        proof = self.cheeseshop_url if status is not self.PY3K_UNKNOWN else None
        self.set_status(status, proof)

    def update_downloads(self, download_urls):
        """Update various download stats about the project."""
        download_total = sum((x.get('downloads', 0) for x in download_urls))
        self.download_total = download_total
        days_available = 1  # Guarantee no DivideByZeroError
        now = datetime.datetime.utcnow()
        for download in download_urls:
            py_version = download.get('python_version')
            if py_version is not None and py_version[0] == '3':
                self.set_status(self.PY3K_RELEASED, self.cheeseshop_url)
            uploaded = download.get('upload_time')
            if uploaded is None:
                logging.warning('No upload_time for {} {}'.format(self.name,
                                                                  self.version))
                return
            # Watch out for clock skew between App Engine and the Cheeseshop!
            days_available += max((now - uploaded).days, 0)
        self.download_rate = download_total / days_available

